<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go 应用代码解析 - 启动与关闭</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>7. 程序启动与优雅关闭 (main 函数)</h1>
        <nav>
            <a href="index.html">概述</a> |
            <a href="01_setup_dependencies.html">1. 依赖与全局变量</a> |
            <a href="02_opentelemetry_init.html">2. OpenTelemetry 初始化</a> |
            <a href="03_database_redis_init.html">3. 数据库与 Redis 初始化</a> |
            <a href="04_web_server_routing.html">4. Web 服务器与路由</a> |
            <a href="05_request_handlers.html">5. HTTP 请求处理</a> |
            <a href="06_business_logic.html">6. 核心业务逻辑</a> |
            <a href="07_main_startup_shutdown.html" class="active">7. 程序启动与关闭</a>
        </nav>
    </header>

    <main>
        <p>`main` 函数是 Go 程序的入口点，整个应用程序从这里开始运行。它负责协调各个组件的初始化、启动核心服务（HTTP 服务器），并在收到退出信号时进行清理工作（优雅关闭）。</p>

        <section id="startup">
            <h2>程序的启动流程</h2>
            <p>`main` 函数按照一定的顺序执行初始化和启动步骤：</p>

            <pre><code class="language-go">func main() {
	// 设置日志格式，包含日期、时间和源码文件及行号
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	log.Println("应用程序启动中...")

	// --- 1. 初始化 OpenTelemetry ---
	log.Println("正在初始化 OpenTelemetry...")
	// 调用我们之前分析过的 initTracerProvider 函数
	shutdown, err := initTracerProvider()
	if err != nil {
		// 如果 OTel 初始化失败，这是致命错误，程序无法继续
		log.Fatalf("致命错误: 初始化 TracerProvider 失败: %v", err)
	}
	// 使用 defer 注册 OTel 的关闭函数
	// defer 语句后面的函数调用会在 main 函数即将退出时执行
	// 这样能确保无论程序是正常结束还是异常中断，都会尝试关闭 OTel
	defer func() {
		// 创建一个带超时的上下文，给 Shutdown 10 秒时间完成
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel() // 确保 cancel 被调用
		log.Println("正在关闭 OpenTelemetry TracerProvider...")
		if err := shutdown(ctx); err != nil {
			// 如果关闭失败，记录错误日志
			log.Printf("错误: 关闭 TracerProvider 时发生错误: %v", err)
		} else {
			log.Println("OpenTelemetry TracerProvider 已成功关闭。")
		}
	}()
	// 获取一个基于全局配置的 Tracer 实例，并赋值给全局变量 tracer
	// "gin-gorm-mysql-redis-app/main" 是这个 Tracer 的名字
	tracer = otel.Tracer("gin-gorm-mysql-redis-app/main")
	log.Println("OpenTelemetry 初始化完成。")

	// --- 2. 初始化数据库 ---
	log.Println("正在初始化数据库连接...")
	// 调用我们之前分析过的 initDB 函数，返回值赋给全局变量 db
	db, err = initDB()
	if err != nil {
		// 数据库初始化失败也是致命错误
		log.Fatalf("致命错误: 初始化数据库失败: %v", err)
	}
	// 尝试获取底层的 *sql.DB 对象，用于配置连接池参数
	sqlDB, err := db.DB()
	if err == nil { // 如果成功获取
		log.Println("正在配置数据库连接池...")
		sqlDB.SetMaxIdleConns(10)     // 设置最大空闲连接数
		sqlDB.SetMaxOpenConns(100)    // 设置最大打开连接数
		sqlDB.SetConnMaxLifetime(time.Hour) // 设置连接可复用的最长时间
		// Ping 数据库确保连接池建立成功
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := sqlDB.PingContext(ctx); err != nil {
			log.Fatalf("致命错误: 数据库 Ping 失败: %v", err)
		}
		log.Println("数据库连接池配置完成并 Ping 成功。")
	} else {
		// 如果获取 *sql.DB 失败，记录警告
		log.Printf("警告: 获取 *sql.DB 对象失败，无法配置连接池: %v", err)
	}
	log.Println("数据库初始化完成。")
	// 注意：数据库连接通常不需要在 defer 中显式关闭，GORM 会管理。
	// 如果直接使用 *sql.DB，则需要在 defer 中关闭。

	// --- 3. 初始化 Redis ---
	log.Println("正在初始化 Redis 连接...")
	// 调用我们之前分析过的 initRedis 函数，返回值赋给全局变量 rdb
	rdb, err = initRedis()
	if err != nil {
		// Redis 初始化失败也是致命错误
		log.Fatalf("致命错误: 初始化 Redis 失败: %v", err)
	}
	// 使用 defer 注册 Redis 的关闭函数
	defer func() {
		log.Println("正在关闭 Redis 连接...")
		if err := rdb.Close(); err != nil {
			log.Printf("错误: 关闭 Redis 连接时发生错误: %v", err)
		} else {
			log.Println("Redis 连接已成功关闭。")
		}
	}()
	log.Println("Redis 初始化完成。")

	// --- 4. 初始化 Gin 引擎 (含中间件) ---
	log.Println("正在初始化 Gin Web 引擎...")
	// 这部分我们在第 4 节已经分析过
	router := gin.New()
	router.Use(otelgin.Middleware("gin-server")) // OTel 中间件
	router.Use(gin.Logger())                    // 日志中间件
	router.Use(gin.Recovery())                   // 恢复中间件
	log.Println("Gin 引擎初始化完成，OTel 和基础中间件已添加。")

	// --- 5. 定义 API 路由 ---
	log.Println("正在定义 API 路由...")
	// 这部分我们在第 4 节也分析过
	userRoutes := router.Group("/users")
	{
		userRoutes.POST("", createUserHandler)
		userRoutes.GET("/:id", getUserHandler)
	}
	router.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "UP"})
	})
	log.Println("API 路由定义完成。")

	// --- 6. 配置并启动 HTTP 服务器 ---
	log.Println("正在配置 HTTP 服务器...")
	// 创建一个标准的 http.Server 实例
	server := &http.Server{
		Addr:         ":8080",         // 监听本地所有 IP 的 8080 端口
		Handler:      router,          // 将配置好路由和中间件的 Gin 引擎作为处理器
		ReadTimeout:  15 * time.Second, // 读取请求超时时间
		WriteTimeout: 15 * time.Second, // 写入响应超时时间
		IdleTimeout:  60 * time.Second, // Keep-Alive 连接的空闲超时时间
	}

	// 使用 go 关键字启动一个新的协程 (goroutine) 来运行 HTTP 服务器
	// 这样 ListenAndServe 就不会阻塞 main 函数的主流程
	go func() {
		log.Printf("HTTP 服务器正在启动，监听地址 %s", server.Addr)
		// ... (省略了启动时打印帮助信息的日志) ...
		log.Printf("-----------------------------------")
		// server.ListenAndServe() 开始监听端口并接受连接
		// 这是一个阻塞操作，直到服务器关闭或发生错误才会返回
		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			// 如果 ListenAndServe 返回错误，并且错误不是 ErrServerClosed
			// (ErrServerClosed 是调用 Shutdown() 时正常返回的错误)，
			// 说明服务器启动失败，记录致命错误并退出程序
			log.Fatalf("致命错误: HTTP 服务器启动失败: %v", err)
		}
		// 如果 ListenAndServe 正常返回 (通常是因为调用了 Shutdown)，打印日志
		log.Println("HTTP 服务器监听协程已正常退出。")
	}() // 别忘了后面的 ()，表示立即执行这个匿名函数

	// --- 7. 处理优雅停机 (下面单独讲) ---
	// ...
}</code></pre>
            <p><strong>启动顺序总结：</strong></p>
            <ol>
                <li><strong>日志设置:</strong> 配置日志输出格式。</li>
                <li><strong>OpenTelemetry 初始化:</strong> 设置追踪系统，获取全局 `tracer`，并用 `defer` 注册关闭函数。</li>
                <li><strong>数据库初始化:</strong> 连接 MySQL，配置连接池，检查连通性。</li>
                <li><strong>Redis 初始化:</strong> 连接 Redis，检查连通性，并用 `defer` 注册关闭函数。</li>
                <li><strong>Gin 初始化:</strong> 创建 Gin 引擎，添加 OTel、Logger、Recovery 中间件。</li>
                <li><strong>路由定义:</strong> 将 URL 路径映射到相应的 Handler 函数。</li>
                <li><strong>HTTP 服务器配置与启动:</strong>
                    <ul>
                        <li>创建一个 `http.Server`，配置监听地址、处理器（Gin router）、超时等参数。</li>
                        <li>在一个新的 goroutine 中调用 `server.ListenAndServe()` 来<strong>异步启动</strong>服务器监听。这样做是为了让 `main` 函数可以继续往下执行，去设置优雅关机的逻辑。</li>
                        <li>检查 `ListenAndServe` 的返回值，如果是非正常关闭的错误，则认为是启动失败。</li>
                    </ul>
                </li>
            </ol>
             <p class="highlight">注意 `defer` 的使用：被 `defer` 的函数调用会按照它们出现的**相反**顺序，在 `main` 函数返回前执行。所以，退出时会先关闭 Redis，然后关闭 OTel。（数据库连接由 GORM 或连接池管理，通常不需要手动 `defer Close()`）。</p>
        </section>

        <section id="shutdown">
            <h2>优雅地谢幕：处理优雅停机 (Graceful Shutdown)</h2>
            <p>当我们需要停止服务时（比如更新版本或维护），我们不希望粗暴地直接掐断所有连接，可能有些用户的请求处理到一半就被中断了。<strong>优雅停机 (Graceful Shutdown)</strong> 的目标是：</p>
            <ul>
                <li>停止接受新的连接请求。</li>
                <li>给当前正在处理的请求一些时间来完成它们。</li>
                <li>完成处理后，再关闭服务器和释放资源（如数据库连接、OTel）。</li>
            </ul>
            <p>Go 的 `os/signal` 包和 `net/http` 服务器的 `Shutdown` 方法让实现这一点变得相对容易。</p>

            <pre><code class="language-go">// (位于 main 函数的末尾)
	// --- 7. 处理优雅停机 ---
	log.Println("设置优雅停机信号监听 (SIGINT, SIGTERM)...")
	// 创建一个 channel，用于接收操作系统信号
	quit := make(chan os.Signal, 1) // 容量为 1 的缓冲 channel

	// signal.Notify 告诉 Go 运行时：如果接收到 SIGINT 或 SIGTERM 信号，
	// 就把这个信号发送到 quit channel 里。
	// SIGINT: 通常由用户按 Ctrl+C 产生。
	// SIGTERM: 通常是系统（如 systemd 或 Docker）要求程序停止时发送的信号。
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	// 程序会阻塞在这里，等待 quit channel 收到信号
	sig := <-quit
	log.Printf("接收到信号 %q，开始执行优雅停机...", sig)

	// 创建一个带有超时时间的上下文，作为 Shutdown 操作的期限
	// 如果 Shutdown 操作超过 25 秒还没完成，就强制停止
	ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
	defer cancel() // 确保 cancel 被调用

	log.Println("正在优雅地关闭 HTTP 服务器...")
	// 调用服务器的 Shutdown 方法，传入带有超时的上下文
	// Shutdown 会：
	// 1. 立即关闭所有的监听器 (不再接受新连接)。
	// 2. 关闭所有空闲的 Keep-Alive 连接。
	// 3. 等待所有正在处理中的请求完成，直到传入的 ctx 超时或取消。
	if err := server.Shutdown(ctx); err != nil {
		// 如果 Shutdown 过程中发生错误 (比如超时)
		log.Printf("错误: HTTP 服务器优雅停机失败: %v", err)
	} else {
		log.Println("HTTP 服务器已优雅关闭。")
	}

	// 当 Shutdown 完成后，main 函数会继续执行到末尾。
	// 这时，之前注册的 defer 函数会按照后进先出的顺序被调用：
	// 1. (Shutdown 的 ctx cancel)
	// 2. Redis 关闭函数 (rdb.Close)
	// 3. (数据库 Ping 的 ctx cancel)
	// 4. OTel 关闭函数 (shutdown)
	// 5. (数据库连接池 Ping 的 ctx cancel) - Gorm 的关闭是隐式的

	log.Println("应用程序即将退出。")
	// main 函数结束，程序退出
}</code></pre>
             <p><strong>优雅关机流程：</strong></p>
             <ol>
                 <li><strong>监听信号:</strong> 使用 `signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)` 设置监听。程序会运行到 `sig := <-quit` 这一行，然后<strong>暂停</strong>，等待 `quit` 通道接收到操作系统发来的 `SIGINT` 或 `SIGTERM` 信号。</li>
                 <li><strong>收到信号:</strong> 当用户按下 Ctrl+C 或系统发送终止信号时，信号会被写入 `quit` 通道。程序从 `<-quit` 处恢复执行，并打印日志表明收到了信号。</li>
                 <li><strong>设置超时上下文:</strong> 创建一个 `context.WithTimeout`，给后续的关闭操作设定一个总的期限（这里是 25 秒）。</li>
                 <li><strong>关闭 HTTP 服务器:</strong> 调用 `server.Shutdown(ctx)`。这是核心步骤。服务器会停止接受新请求，并等待现有请求处理完毕。如果所有请求在 25 秒内处理完了，`Shutdown` 会正常返回 `nil` 或 `http.ErrServerClosed`。如果超过 25 秒还有请求没处理完，`Shutdown` 会返回一个超时错误，并且强制关闭剩余连接。</li>
                 <li><strong>执行 `defer` 清理:</strong> 当 `Shutdown` 返回后（无论成功还是超时），`main` 函数即将结束。此时，之前用 `defer` 注册的函数会按相反顺序执行：先是 `rdb.Close()` 关闭 Redis 连接，然后是 OTel 的 `shutdown(ctx)` 函数被调用，确保 OTel 的数据被导出。</li>
                 <li><strong>程序退出:</strong> `main` 函数执行完毕，整个程序退出。</li>
             </ol>
             <p>通过这样的流程，服务可以在收到停止指令后，尽量完成当前的工作再退出，减少对用户的影响，并确保资源被正确释放。</p>
        </section>

        <section id="conclusion">
            <h2>总结</h2>
            <p>我们已经完整地分析了这个 Go 程序：</p>
            <ul>
                <li>从 `main` 函数开始，依次初始化了 OTel、数据库 (MySQL + GORM + otelgorm)、缓存 (Redis + redisotel)。</li>
                <li>设置了 Gin Web 框架，并集成了 OTel 中间件 (`otelgin`)、日志和恢复中间件。</li>
                <li>定义了处理用户创建 (`POST /users`) 和查询 (`GET /users/:id`) 的 API 路由，并将它们指向对应的 Handler 函数 (`createUserHandler`, `getUserHandler`)。</li>
                <li>Handler 函数负责解析请求、启动自己的 OTel Span，然后调用业务逻辑层的 Service 函数 (`saveUser`, `findUserByID`)。</li>
                <li>Service 函数也启动自己的 OTel Span，执行核心业务逻辑：
                    <ul>
                        <li>`findUserByID` 实现了缓存优先 (Cache-Aside) 策略，先查 Redis，未命中再查 MySQL，查到后写回 Redis。</li>
                        <li>`saveUser` 负责将用户数据写入 MySQL，然后执行缓存失效（删除 Redis 中的 Key）。</li>
                    </ul>
                    数据库和 Redis 的操作因为集成了 OTel 插件，会自动被追踪。
                </li>
                 <li>最后，`main` 函数启动了 HTTP 服务器来监听请求，并通过信号监听和 `server.Shutdown` 实现了优雅停机。</li>
            </ul>
             <p>这个项目虽然简单，但它演示了构建一个包含 Web 服务、数据库、缓存和可观测性 (Tracing) 的现代 Go 应用的基本要素和最佳实践。</p>
             <p><a href="index.html">返回程序概述</a></p>
        </section>
    </main>

    <footer>
        <p>Go 应用代码解析</p>
    </footer>
</body>
</html>